if (!folderplus)
	var folderplus = {};

folderplus.conv = {

    SEARCH_TIMEOUT : 3000,	
    
    searchLevel : 2,

	searchPrefixe : "re|fw|fwd|tr|fyi|urgent",
	
	// ============================================================================================================
	// Find conversation functions                                                                 
	// ============================================================================================================
	
	/**
	 * set move to conversation and go to folder menu in mailContext menu
	 * and go menu.
	 * Async method :-/ First call a globa search to get a gloda message from 
	 * aMsgHdr then call a second gloda search to find all messages from the 
	 * conversation.
	 * @param aMsgHdr,    use to search conversation folder
	 * @param callback,   a callback called when folder is found to make an action, 
	 * @param Gloda,      Gloda engine to use
	 * @param callbackUI, use to search conversation folder
	 * @return nothing, use the callback.
	 *    callback param    : a folder uri, "error" => "?", null => ?. Only called if a folder is found.
	 *    callbackUI param  : a folder uri, "error" => "?", null => ?.
	 */
	_getFolder : function(aMsgHdr, callback, Gloda, callbackUI) {
		// cf msgHdrViewOverlay.js/ConversationOpener
		/* First, look for a gloda message from messege header. */
		var aListener = {
			onItemsAdded : function myListener_onItemsAdded(aItems, aCollection) {},
			onItemsModified : function myListener_onItemsModified(aItems,aCollection) {},
			onItemsRemoved : function myListener_onItemsRemoved(aItems,aCollection) {},
			onQueryCompleted : function myListener_onQueryCompleted(aCollection) {
				try {
					var listenerStop = new Date();
					folderplus.stats.step(aStat, "getGloda");
					if (abort) {
						folderplus.log("search aborted listener");
						throw "abort timeout";
					}
					if (!aCollection.items.length) {
                        folderplus.log("can't get glodaMessage from msgHdr");
                        folderplus.stats.glodaMissed();
						throw "can't get glodaMessage";
					} else {
						var aGlodaMessage = aCollection.items[0];
						// setTimeout généré dans les recherches gloda (et pas avant)
						// sinon si plusieurs timeout sont empilés, ils sont tous
						// lancés en même temps alors que les recherches gloda
						// s'empilent car une seule à la fois.
						setTimeout(function () {
							folderplus.log("timeout reached");
							abort=true;
						}, folderplus.conv.SEARCH_TIMEOUT);
						var aConvCollection = aGlodaMessage.conversation.
							getMessagesCollection(aConvListener);
					}
				} catch (e) {
					Components.utils.reportError(e);
					if (callback) callback("error");
					callbackUI("error");
					folderplus.stats.end(aStat, "message", e);
				}
			}
		};
		/* Second, look for a conversation from gloda message. */
		var aConvListener = {
			onItemsAdded : function myListener_onItemsAdded(aItems, aCollection) {},
			onItemsModified : function myListener_onItemsModified(aItems,aCollection) {},
			onItemsRemoved : function myListener_onItemsRemoved(aItems,aCollection) {},
			onQueryCompleted : function myListener_onQueryCompleted(aCollectionConv) {
				try {
					var listenerStop = new Date();
					folderplus.stats.step(aStat, "conv");
					if (abort) {
						folderplus.log("search aborted");
						throw "abort timeout";
					}
					if (!aCollectionConv.items.length) {
                        // deal as an error as we should have at least one message
						throw "Couldn't find conversation message for msg: " + aMsgHdr.messageId;
					}
					var msgConvCount = aCollectionConv.items.length;
					var isAllConvInFolder = folderplus.conv._isAllConvInFolder(aCollectionConv.items, aMsgHdr.folder.URI);
					if (msgConvCount == 1 || isAllConvInFolder) {
						// si le message ne fait parti d'aucune conversation
						// ou qu'il est le 1° d'une conversation qui commence
						// try to found folder by subject
						var aSubject = aMsgHdr.mime2DecodedSubject;
						if (folderplus.conv.searchLevel > 0 && aSubject != "") {
							// need to remove with my function to remove 'fw:'
							aSubject = folderplus.conv._getPrefixeRemoveREFWD(aSubject);
							aSubject = "\"" + aSubject + "\"";
							/** gloda search on subject is case-insensitive, accent-insensitive, not exact,
							 * seems to automaticaly remove re:, tr:, use operator as '-' so need double quotes.
							 * [Migration SVN] : compte-rendu de réunion => 0 result
							 * \"[Migration SVN] : compte-rendu de réunion\" => 6 results
							 * \"RE: [Migration SVN] : compte-rendu de réunion\" => 6 results
							 */
							folderplus.log("searching by subject \"" + aSubject.substr(0,15) + "\" (only one message found or many but all in current folder)");
							var query=Gloda.newQuery(Gloda.NOUN_MESSAGE);
							query.subjectMatches(aSubject);
							query.getCollection(aSubjectListenerExact);
						} else {
							folderplus.log("no result : no message found by conv and (no subject to use or search limited to conv)");
							callbackUI(null);
							folderplus.stats.end(aStat, "conv", null);
						}
					} else {
						folderplus.log("founded by conversation => look for rigth folder in conversation");
						var folderURI = folderplus.conv._findConversationFolder(aCollectionConv.items, aMsgHdr);
						folderplus.conv._lastSearch.messageId = aMsgHdr.messageId;
						folderplus.conv._lastSearch.folderURI = folderURI;
						folderplus.conv._lastSearch.timestamp = new Date();
						if (callback != null) callback(folderURI);
						callbackUI(folderURI);
						folderplus.stats.end(aStat, "conv", folderURI);
					}    					
				} catch (e) {
					Components.utils.reportError(e);
					if (callback) callback("error");
					callbackUI("error");
					folderplus.stats.end(aStat, "conv", e);
				}
			}
		};
		/* Third, look for message by subject. */
		var aSubjectListenerExact = {
			onItemsAdded : function myListener_onItemsAdded(aItems, aCollection) {},
			onItemsModified : function myListener_onItemsModified(aItems,aCollection) {},
			onItemsRemoved : function myListener_onItemsRemoved(aItems,aCollection) {},
			onQueryCompleted : function myListener_onQueryCompleted(aCollection) {
				try {
					var listenerStop = new Date();
					folderplus.stats.step(aStat, "subject");
					if (abort) {
						folderplus.log("search aborted");
						throw "abort timeout";
					}
                    var msgConvCount = aCollection.items.length;
					//var isNewer = folderplusConv._isNewerConv(aCollection.items, aMsgHdr.date);
					var isAllConvInFolder = folderplus.conv._isAllConvInFolder(aCollection.items, aMsgHdr.folder.URI)
					if (msgConvCount <= 1 || isAllConvInFolder) {
						// try to found folder by prefix
						var aSubject = aMsgHdr.mime2DecodedSubject;
						var aPrefix = folderplus.conv._getPrefixe(aSubject);
						if (folderplus.conv.searchLevel > 1 && aPrefix) {
							aSubject = "\"" + aPrefix + "\"";
							folderplus.log("searching by prefix " + aSubject + " ( 0 or 1 message found or all in current folder)");
							var query=Gloda.newQuery(Gloda.NOUN_MESSAGE);
							query.subjectMatches(aSubject);
							query.getCollection(aPrefixListener);
						} else {
							folderplus.log("no result : no message found by subject (0 or 1 message found by subject or all in current forder) and no prefix to use.");
							// null => use current folder.
							folderplus.conv._lastSearch.messageId = aMsgHdr.messageId;
							folderplus.conv._lastSearch.folderURI = null;
							folderplus.conv._lastSearch.timestamp = new Date();
							if (callback != null) callback(null);
							callbackUI(null);
							folderplus.stats.end(aStat, "subject", null);
						}
					} else {
						folderplus.log("found by subject ( >= 2 message and some one outside current folder) => look for right folder.");						
						var folderURI = folderplus.conv._findConversationFolder(aCollection.items, aMsgHdr);
						folderplus.conv._lastSearch.messageId = aMsgHdr.messageId;
						folderplus.conv._lastSearch.folderURI = folderURI;
						folderplus.conv._lastSearch.timestamp = new Date();
						if (callback) callback(folderURI);				
						callbackUI(folderURI);			
						folderplus.stats.end(aStat, "subject", folderURI);
					}
				} catch (e) {
					Components.utils.reportError(e);
					if (callback) callback("error");
					callbackUI("error");
					folderplus.stats.end(aStat, "subject", e);
				}
			}
		};
		/* fourth , look for message by prefix. */
		var aPrefixListener = {
			onItemsAdded : function myListener_onItemsAdded(aItems, aCollection) {},
			onItemsModified : function myListener_onItemsModified(aItems,aCollection) {},
			onItemsRemoved : function myListener_onItemsRemoved(aItems,aCollection) {},
			onQueryCompleted : function myListener_onQueryCompleted(aCollection) {
				try {
					var listenerStop = new Date();
					folderplus.stats.step(aStat, "prefix");
					if (abort) {
						folderplus.log("search aborted");
						throw "abort timeout";
					}
					if (!aCollection.items.length) {
						folderplus.log("no message found by prefix");
						// not considered as error => no throws
						callback(null);
						// inutile d'appeler callbackUI("")
						folderplus.stats.end(aStat, "prefixe", null);
					} else {
						folderplus.log("Found conversation by prefix => look for the right folder.");
						var folderURI = folderplus.conv._findConversationFolder(aCollection.items, aMsgHdr);
						folderplus.conv._lastSearch.messageId = aMsgHdr.messageId;
						folderplus.conv._lastSearch.folderURI = folderURI;
						folderplus.conv._lastSearch.timestamp = new Date();
						if (callback) callback(folderURI);				
						callbackUI(folderURI);				
						folderplus.stats.end(aStat, "prefixe", folderURI);
					}
				} catch (e) {
					Components.utils.reportError(e);
					if (callback) callback("error");
					callbackUI("error");
					folderplus.stats.end(aStat, "prefixe", e);
				}
			}
		};
 		/* First, look for a gloda message from messege header. */
		if (aMsgHdr == null) {
			Components.utils.reportError("Message is null");
			if (callback) callback("error");
			callbackUI("error");
		} else {
			var aStat = new folderplus.stats.stats();
            var searchStart = new Date();
			// la variable sera closurée par le timeout.
			// chaque recherche gloda a donc son propre abort.
			var abort = false;
			folderplus.log("starting founding conversation for message \"" + aMsgHdr.mime2DecodedSubject + "\"");
			if (this._lastSearch.messageId != aMsgHdr.messageId ||
			    searchStart - this._lastSearch.timestamp > 1 * 10 * 1000) {
				callbackUI("");		
				var aCollection = Gloda.getMessageCollectionForHeader(aMsgHdr, aListener);
			} else {
				folderplus.log("use cache");
				if (callback) callback(this._lastSearch.folderURI);
				folderplus.stats.end(aStat, "cache", this._lastSearch.folderURI);
			}
		}
	},
	
	/**
	 * test if all messages inside the conv are newer that a specified date.
	 * @returns boolean, true if conv is empty
	 */
	_isNewerConv : function (aConvItems, aMessageDate) {
		for (var i = 0; i < aConvItems.length; i++) {
			var aDate = aConvItems[i].folderMessage.date;
			if (aDate < aMessageDate) {
				return false;
			}
		}
		return true;
	},

	/**
     * check si tous les messages sont dans le folder spécifié
	 * @param String folderURI
	 * @returns boolean, true if conv is empty
	 */
	_isAllConvInFolder : function (aConvItems, aFolderURIComp) {
		for (var i = 0; i < aConvItems.length; i++) {
			if (aConvItems[i].folderMessage) {
				var aFolderURI = aConvItems[i].folderMessage.folder.URI;
				if (aFolderURI != aFolderURIComp) {
					return false;
				}
			} else {
				folderplus.log("can't get folderMessage of gloda message" + aConvItems[i].subject + ", " + aConvItems[i].id);
                folderplus.stats.glodaMissed();
				// Pas de throw pour tenter de passage au message suivant.
			}
		}
		return true;
	},

	/*
	 * Array[String], empty array if no blackList
	 * "".split("|") = [""] (an array of one empty string).
	 */
	_blackListURIs : new Array(""),
	
	/*
	 * Array[String], empty array if no blackList
	 * "".split("|") = [""] (an array of one empty string).
	 */
	_lastSearch : { messageId : null, folderURI : null, timestamp : null },
	
	/*
	 * update the black list when a folder changes.
	 */
	_updateBlackListWithURI : function (aOrigFolderURI, aNewFolderURI) {
		var needWrite = false;
		for (var i = 0; i < this._blackListURIs.length; i++) {
		  var reg = new RegExp(aOrigFolderURI + "(?=/|$)");
			var newURI = this._blackListURIs[i].replace(reg, aNewFolderURI);
			this._blackListURIs[i] = newURI;
			needWrite = true;
		}
		return needWrite;
	},
	
	/**
	 * This function decides which Folder use as the "conversation folder".
	 * Look for the older message received in the conversation. Don't use the messages
	 * inside the current folder to avoid to return the 'inbox'. Don't use message newer 
	 * than current message.	 
	 * @params messages collection
	 * @param  currentMessage MsgHdr	 
	 * @return String folderURI or null, null if items.length == 0
	 */
	_findConversationFolder : function (items, currentMessage) {
		var olderDate = null;
		var olderMessage = null;
		var currentMessageDate   = currentMessage.date;
		var currentMessageFolder = currentMessage.folder.URI;
        //folderplus.log("currentMessageDate " + currentMessageDate + ", currentMessageFolder" + currentMessageFolder);
        var findStart = new Date();
		for ( var i = 0; i < items.length; i++) {
			//folderplus.log("_findConversationFolder:searchStart"+searchStart);					
			//if (abort) {
			//	folderplus.log("search aborted");
			//	throw "abort timeout";
			//}
			var aItem = items[i];
            if (aItem.folderMessage) {
				//folderplus.log(aItem.subject + ", " + aItem.folderMessage.folder.URI + ", " + aItem.folderMessage.date);
				var aDate = aItem.folderMessage.date;
				var aFolder = aItem.folderMessage.folder.URI;
				//@ test vérifier la prise en compte de la blacklist dans la détermination de la conversation
				// folderplus.log("_findConversationFolder " + (aDate < currentMessageDate) + "," + (aFolder != currentMessageFolder) );
                if ((aDate < currentMessageDate)
				    && (aFolder != currentMessageFolder)
				    && (olderDate == null || (olderDate < aDate)))
				{
					
                    //@ todo déplacer les folder utile au blacklistage au début du tableau pour optimiser les perfs
					//@ todo Optimiser l'algorithme. Tenir compte d'une potentielle récursivité du blacklistage.
					var isBlackListed = this._blackListURIs.indexOf(aFolder) >= 0;
					if (!isBlackListed) {
						olderDate    = aItem.folderMessage.date;
						olderMessage = aItem; 
					} else {
						folderplus.log("blackListed " + aFolder + " found");
					}
				}
			} else {
				folderplus.log("can't get folderMessage of gloda message" + aItem.subject + ", " + aItem.folderURI + ", " + aItem.id);
                folderplus.stats.glodaMissed();
				// Pas de throw pour tenter de passage au message suivant.
			}
		}
		var findEnd = new Date();
		folderplus.log("Folder choose duration [ms] " + (findEnd - findStart));
		if (olderMessage) {
			folderplus.log("Folder chosen : " + olderMessage.folderMessage.folder.URI);
			return olderMessage.folderMessage.folder.URI;
		} else {
			folderplus.log("No folder to choose (newer, blacklisted, inside current older)");
			return null;
		}
	},
	
	_getPrefixeRemoveREFWD : function (aSubject) {
		var regex = new RegExp("((" + folderplus.conv.searchPrefixe + ") ?)+: ?", "gi");
		var result = aSubject.replace(regex, "");
		return result;
	},
	
	_getPrefixe : function (aSubject) {
		var result = null;
		var aSubjectToPrefixe = folderplus.conv._getPrefixeRemoveREFWD(aSubject);
		var regBracket = /\[(.+)\]/;		
		var resultBracket = regBracket.exec(aSubjectToPrefixe);
		if (resultBracket == null) {
			// make regexp not greedy with .+?
			var regDot = /(.+?) ?([:/]| -)/;
			var resultDot = regDot.exec(aSubjectToPrefixe);
			if (resultDot != null) {
				result = resultDot[1].trim();
			}
		} else {
			result = resultBracket[1].trim();
		}
		/* to blacklist some prefix
		if (result.toLowerCase() == "infogreffe") {
			folderplus.log("Prefixe cancelled : " + result);
			result = null;
		}.
		*/
		return result;
	}	
}

folderplus.stats = {

    stats : function () {
        this.start = new Date();
    },
    
    sessionSearchCount : 0,
    sessionSearchTotalTime : 0,
    lastStat : null,
    
   step : function (aStat, aStep) {
        var step = { step : aStep, time : new Date() };
		if (!aStat.steps) {
			aStat.steps = new Array();
		}
        aStat.steps.push(step);
   },

   end : function (aStat, searchLevel, result) {
        aStat.end = new Date();
        aStat.duration = aStat.end - aStat.start;
        aStat.searchLevel = searchLevel;
        aStat.result = result;
        this.lastStat = aStat;
        this.sessionSearchCount += 1;
        this.sessionSearchTotalTime += aStat.duration;
    },
    
    sessionGlodaMessageMissed : 0,
    
    glodaMissed : function () {
        this.sessionGlodaMessageMissed += 1;
    },
    
    toString : function (aStat) {
        var searchResult = aStat.result == null ? "null" : aStat.result == "" ? "chaine vide" : aStat.result.substr(-10);
		var result = "duration:" + aStat.duration + ",\nsearchLevel:" + aStat.searchLevel + ",result:" + searchResult;
		if (aStat.steps) {
			for (var i = 0; i < aStat.steps.length; i++) {
				var stepPrev =  i == 0 ? aStat.start : aStat.steps[i-1].time;
				var timeStep =  aStat.steps[i].time;
				result += "," + aStat.steps[i].step + ":" + (timeStep - stepPrev);
			}
		}
        return result;
    }
}
